# base stage
FROM ubuntu:22.04 AS base
USER root
SHELL ["/bin/bash", "-c"]
# 上面把ubuntu:22.04作为基础镜像，用户角色是root，指定shell

# 修改国内镜像进行构建，如果是国内镜像，改成 ARG NEED_MIRROR=1
ARG NEED_MIRROR=1
# LIGHTEN 参数用于控制是否轻量化构建，默认为 0（不轻量）
ARG LIGHTEN=0

# 将 LIGHTEN 环境变量导出到容器中，便于后续判断是否执行部分操作
ENV LIGHTEN=${LIGHTEN}

# 设置工作目录为 /ragflow，后续所有操作都在此目录下进行
WORKDIR /ragflow

# 创建 deepdoc 资源目录和用户目录下的 .ragflow 目录，用于存放模型文件
# Copy models downloaded via download_deps.py
RUN mkdir -p /ragflow/rag/res/deepdoc /root/.ragflow

# 挂载 infiniflow/ragflow_deps:latest 镜像中的 huggingface.co 数据到容器，并复制 huqie.txt.trie 文件到资源目录
# --mount=type=bind: 使用 Docker BuildKit 的 --mount 选项来挂载一个卷（volume）。这里使用的是 bind 类型，意味着将宿主机上的某个目录或另一个镜像中的目录挂载到容器中。
# from=infiniflow/ragflow_deps:latest: 指定要从哪个镜像挂载内容。这里是 infiniflow/ragflow_deps:latest 镜像。
# source=/huggingface.co: 指定源路径，即从 infiniflow/ragflow_deps:latest 镜像中的 /huggingface.co 目录。
# target=/huggingface.co: 指定目标路径，即将上述源路径挂载到当前构建阶段容器的 /huggingface.co 目录。
# cp /huggingface.co/InfiniFlow/huqie/huqie.txt.trie /ragflow/rag/res/cp 源文件路径，位于挂载的 infiniflow/ragflow_deps:latest 镜像中的 /huggingface.co/InfiniFlow/huqie/ 目录下。 /ragflow/rag/res/: 目标路径，表示将 huqie.txt.trie 文件复制到当前容器的 /ragflow/rag/res/ 目录下。
# tar --exclude='.*' -cf - tar: 打包工具。--exclude='.*': 排除所有以点开头的隐藏文件（如 .git, .DS_Store 等）-cf -: 将打包的内容输出到标准输出（stdout），而不是写入文件。这里的 - 表示标准输出。这些是 tar 命令要打包的目录列表：/huggingface.co/InfiniFlow/text_concat_xgb_v1.0和/huggingface.co/InfiniFlow/deepdoc 
# | tar -xf - --strip-components=3 -C /ragflow/rag/res/deepdoc 的解释|: 管道符号，将前一个命令的标准输出作为下一个命令的标准输入。tar -xf -: 解压来自标准输入（stdin）的数据。--strip-components=3: 解压时去掉前 3 级目录结构。例如，如果原始路径是/huggingface.co/InfiniFlow/deepdoc/some/path/file.txt，解压后会变成 some/path/file.txt。-C /ragflow/rag/res/deepdoc: 解压到指定的目标目录 /ragflow/rag/res/deepdoc。
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/huggingface.co,target=/huggingface.co \
    cp /huggingface.co/InfiniFlow/huqie/huqie.txt.trie /ragflow/rag/res/ && \
    tar --exclude='.*' -cf - \
        /huggingface.co/InfiniFlow/text_concat_xgb_v1.0 \
        /huggingface.co/InfiniFlow/deepdoc \
        | tar -xf - --strip-components=3 -C /ragflow/rag/res/deepdoc 
# 再次挂载依赖镜像，根据 LIGHTEN 变量决定是否复制大模型数据到 /root/.ragflow
# --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/huggingface.co,target=/huggingface.co type=bind：表示将源路径绑定挂载到目标路径。from=infiniflow/ragflow_deps:latest：指定要挂载的镜像来源。source=/huggingface.co：该镜像中我们要使用的目录路径。target=/huggingface.co：在当前构建容器中挂载为 /huggingface.co 路径。
# $LIGHTEN 是一个环境变量或构建参数（来自前面的 ARG LIGHTEN=0 和 ENV LIGHTEN=${LIGHTEN}）。如果 LIGHTEN != "1"，则执行括号内的命令。当 LIGHTEN=0 时包含完整模型数据，适合生产部署。tar -cf - ：将指定路径下的两个模型目录打包成 .tar 流，并输出到标准输出（stdout）。-c：创建新归档；-f -：输出到标准输出；| 管道符 tar -xf - --strip-components=2 -C /root/.ragflow -x：解压；-f -：从标准输入读取；--strip-components=2：去掉前两级目录结构；-C /root/.ragflow：解压到 /root/.ragflow 目录下。
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/huggingface.co,target=/huggingface.co \
    if [ "$LIGHTEN" != "1" ]; then \
        (tar -cf - \
            /huggingface.co/BAAI/bge-large-zh-v1.5 \
            /huggingface.co/maidalun1020/bce-embedding-base_v1 \
            | tar -xf - --strip-components=2 -C /root/.ragflow) \
    fi

# https://github.com/chrismattmann/tika-python
# This is the only way to run python-tika without internet access. Without this set, the default is to check the tika version and pull latest every time from Apache.
# 使用 Docker BuildKit 的 --mount 功能，将另一个镜像的内容挂载进当前构建的容器中。type=bind：表示绑定挂载；from=infiniflow/ragflow_deps:latest：源镜像为 infiniflow/ragflow_deps:latest；source=/：挂载该镜像的根目录 /；target=/deps：将其挂载到当前容器的 /deps 目录下。
# 递归复制 /deps/nltk_data 目录到 /root/ 下。 复制 Apache Tika 的服务器端 jar 包及其 MD5 校验文件到 /ragflow/ 目录。
# cp /deps/cl100k_base.tiktoken /ragflow/9b5ad71b2ce5302211f9c61530b329a4922fc6a4
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/,target=/deps \
    cp -r /deps/nltk_data /root/ && \
    cp /deps/tika-server-standard-3.0.0.jar /deps/tika-server-standard-3.0.0.jar.md5 /ragflow/ && \
    cp /deps/cl100k_base.tiktoken /ragflow/9b5ad71b2ce5302211f9c61530b329a4922fc6a4

# 设置 TIKA_SERVER_JAR 环境变量，指向本地的 tika-server jar 文件
ENV TIKA_SERVER_JAR="file:///ragflow/tika-server-standard-3.0.0.jar"
# 设置 DEBIAN_FRONTEND 为 noninteractive，避免安装时出现交互式提示
ENV DEBIAN_FRONTEND=noninteractive

# Setup apt
# Python package and implicit dependencies:
# opencv-python: libglib2.0-0 libglx-mesa0 libgl1
# aspose-slides: pkg-config libicu-dev libgdiplus         libssl1.1_1.1.1f-1ubuntu2_amd64.deb
# python-pptx:   default-jdk                              tika-server-standard-3.0.0.jar
# selenium:      libatk-bridge2.0-0                       chrome-linux64-121-0-6167-85
# Building C extensions: libpython3-dev libgtk-4-1 libnss3 xdg-utils libgbm-dev
# 使用 BuildKit 的 cache mount 功能，将 apt 缓存挂载到容器中以加速后续构建并减少重复下载
# --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked
#   --mount=type=cache 的作用  Docker BuildKit 提供的 cache mount 功能，在多个构建之间共享 /var/cache/apt
#   id=ragflow_apt：缓存标识符，用于唯一识别这个缓存
#   sharing=locked：表示该缓存一次只能被一个构建进程使用，避免冲突。
# if 这一段， 如果设置了 NEED_MIRROR=1，则将官方源替换为清华源。
# chmod 1777 设置 /tmp 目录权限为 1777，即 rwxrwxrwt。 所有用户都可以读写；每个用户只能删除自己的文件（t 表示粘滞位）
RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
    # 如果 NEED_MIRROR 被设置为 1，则将 Ubuntu 官方源替换为清华大学的镜像源，加快国内下载速度
    if [ "$NEED_MIRROR" == "1" ]; then \
        sed -i 's|http://ports.ubuntu.com|http://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list; \
        sed -i 's|http://archive.ubuntu.com|http://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list; \
    fi; \
    # 删除 Docker 默认的 apt 清理配置，防止安装后自动清理缓存包
    rm -f /etc/apt/apt.conf.d/docker-clean && \
    # 配置 apt 保留已下载的安装包，便于后续复用和离线安装
    echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache && \
    # 设置 /tmp 目录权限为所有用户可读写执行，避免临时文件操作失败
    chmod 1777 /tmp && \
    # 更新软件包列表，准备安装所需依赖
    apt update && \
    # 安装 ca-certificates 包，用于支持 HTTPS 通信
    apt --no-install-recommends install -y ca-certificates && \
    # 再次更新软件包列表（确保前面的更改生效）
    apt update && \
    # 安装图形库 libglib2.0-0、libglx-mesa0 和 libgl1，供图像处理或 GUI 应用使用
    apt install -y libglib2.0-0 libglx-mesa0 libgl1 && \
    # 安装编译工具 pkg-config 及国际化支持库 libicu-dev 和 GDI+ 兼容库 libgdiplus
    apt install -y pkg-config libicu-dev libgdiplus && \
    # 安装 Java 开发环境 default-jdk，满足某些服务对 Java 的依赖
    apt install -y default-jdk && \
    # 安装辅助功能库 libatk-bridge2.0-0，用于无障碍访问支持
    apt install -y libatk-bridge2.0-0 && \
    # 安装 Python 开发库、GTK4、NSS 加密库及 xdg-utils 工具集，提升兼容性
    apt install -y libpython3-dev libgtk-4-1 libnss3 xdg-utils libgbm-dev && \
    # 安装内存分配库 libjemalloc-dev，提高内存管理性能
    apt install -y libjemalloc-dev && \
    # 安装常用工具：pip（Python 包管理器）、pipx（独立虚拟环境安装）、nginx（反向代理服务器）、unzip、curl、wget、git、vim、less
    apt install -y python3-pip pipx nginx unzip curl wget git vim less && \
    # 安装 ghostscript，用于 PDF 等文档的渲染与转换
    apt install -y ghostscript

# 如果 NEED_MIRROR 为 1，则配置 pip3 使用阿里云 PyPI 镜像以加速国内下载
RUN if [ "$NEED_MIRROR" == "1" ]; then \
        # 设置 pip3 默认使用阿里云镜像源
        pip3 config set global.index-url https://mirrors.aliyun.com/pypi/simple && \
        pip3 config set global.trusted-host mirrors.aliyun.com; \
        # 创建uv的设置目录
        mkdir -p /etc/uv && \
        # 开始写入 uv.toml 配置文件，设置阿里云镜像源
        echo "[[index]]" > /etc/uv/uv.toml && \
        # 指定 uv 使用的镜像地址为阿里云 PyPI 源
        echo 'url = "https://mirrors.aliyun.com/pypi/simple"' >> /etc/uv/uv.toml && \
        # 标记该镜像为默认使用的源
        echo "default = true" >> /etc/uv/uv.toml; \
    fi; \
    # 使用 pipx 安装 Python 包管理工具 uv（一个快速的 Python 包安装工具）pipx 是专门用于在隔离环境中安装和运行 Python 应用程序的工具；
    pipx install uv

ENV PYTHONDONTWRITEBYTECODE=1 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV PATH=/root/.local/bin:$PATH

# nodejs 12.22 on Ubuntu 22.04 is too old
# 使用 BuildKit 的 cache mount 功能缓存 apt 数据，提升后续构建速度
RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
    # 下载并执行 NodeSource 提供的 Node.js 20.x 安装脚本
    curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
    # 卸载已存在的 nodejs、npm 和 cargo（cargo 是 Rust 工具链的一部分）
    apt purge -y nodejs npm cargo && \
    # 删除不再需要的依赖包，清理系统
    apt autoremove -y && \
    # 更新软件包列表以确保获取最新的软件源信息
    apt update && \
    # 安装 Node.js 运行时环境
    apt install -y nodejs

# A modern version of cargo is needed for the latest version of the Rust compiler.
# 更新 apt 包列表并安装 curl 和 build-essential（用于编译和构建工具）
RUN apt update && apt install -y curl build-essential \
    # 如果 NEED_MIRROR 为 1，则配置 Rustup 使用清华大学 TUNA 镜像加速下载
    && if [ "$NEED_MIRROR" == "1" ]; then \
         # Use TUNA mirrors for rustup/rust dist files
        #  设置 RUSTUP_DIST_SERVER 环境变量为 TUNA 的镜像地址，用于下载 Rust 工具链
         export RUSTUP_DIST_SERVER="https://mirrors.tuna.tsinghua.edu.cn/rustup"; \
         # 设置 RUSTUP_UPDATE_ROOT 环境变量为 TUNA 的 rustup 更新根路径
         export RUSTUP_UPDATE_ROOT="https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup"; \
         # 输出提示信息：正在使用 TUNA 镜像加速 Rustup 下载
         echo "Using TUNA mirrors for Rustup."; \
       fi; \
    # Force curl to use HTTP/1.1
    # 使用 curl 下载 Rustup 安装脚本，并强制使用 HTTP/1.1 协议以避免 CDN 兼容性问题 --proto '=https'：只允许 HTTPS 协议；--tlsv1.2：强制使用 TLS 1.2 加密协议；--http1.1：避免使用 HTTP/2，某些 CDN 对 HTTP/2 支持有问题； bash -s 表示从标准输入（stdin）读取脚本内容，而不是从文件。-- 分隔符，表示后面的所有内容都是传递给脚本的参数，而不是 bash 的选项。--profile minimal：仅安装最小化的 Rust 工具集（rustc + cargo）
    curl --proto '=https' --tlsv1.2 --http1.1 -sSf https://sh.rustup.rs | bash -s -- -y --profile minimal \
    # 将 Rust 的 cargo bin 目录添加到 root 用户的 PATH 环境变量中
    && echo 'export PATH="/root/.cargo/bin:${PATH}"' >> /root/.bashrc

ENV PATH="/root/.cargo/bin:${PATH}"

RUN cargo --version && rustc --version

# Add msssql ODBC driver
# macOS ARM64 environment, install msodbcsql18.
# general x86_64 environment, install msodbcsql17.
# 添加 Microsoft 的 GPG 密钥到 apt，用于验证后续软件包的合法性
RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
# 配置 Microsoft SQL Server 的 APT 源到系统中，以便安装相关组件
    curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \
# 将 Microsoft SQL Server 的源列表文件写入指定目录，配置适用于 Ubuntu 22.04 的仓库
    curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list > /etc/apt/sources.list.d/mssql-release.list && \
# 更新 apt 包索引，确保可以获取到新添加源中的软件包信息
    apt update && \
# 获取当前系统的架构类型（如 x86_64、arm64 等）
    arch="$(uname -m)"; \
# 判断当前系统是否为 ARM64 架构（例如 macOS Apple Silicon 或 Linux aarch64）
    if [ "$arch" = "arm64" ] || [ "$arch" = "aarch64" ]; then \
        # ARM64 (macOS/Apple Silicon or Linux aarch64)
        # 对于 ARM64 架构，接受 EULA 并安装 unixodbc-dev 和 msodbcsql18（ODBC 驱动）
        ACCEPT_EULA=Y apt install -y unixodbc-dev msodbcsql18; \
    else \
        # x86_64 or others
        # 对于 x86_64 或其他架构，接受 EULA 并安装 unixodbc-dev 和 msodbcsql17（ODBC 驱动）
        ACCEPT_EULA=Y apt install -y unixodbc-dev msodbcsql17; \
    fi || \
# 如果上述命令执行失败，则输出错误信息并退出
    { echo "Failed to install ODBC driver"; exit 1; }



# Add dependencies of selenium
    # 添加 selenium 所需的依赖项：Chrome 浏览器二进制文件 需要目标镜像 infiniflow/ragflow_deps:latest 中已经包含所需的文件。
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/chrome-linux64-121-0-6167-85,target=/chrome-linux64.zip \
    # 解压下载的 Chrome 压缩包
    unzip /chrome-linux64.zip && \
    # 将解压后的目录重命名为 /opt/chrome
    mv chrome-linux64 /opt/chrome && \
    # 创建软链接，使 chrome 可在命令行中直接调用
    ln -s /opt/chrome/chrome /usr/local/bin/
    # 添加 selenium 所需的 chromedriver 驱动程序
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/chromedriver-linux64-121-0-6167-85,target=/chromedriver-linux64.zip \
    # 解压 chromedriver 的压缩包，并只提取 chromedriver 可执行文件 -j 表示“junk paths”，即忽略 ZIP 文件中的目录结构，直接解压出文件。
    unzip -j /chromedriver-linux64.zip chromedriver-linux64/chromedriver && \
    # 将 chromedriver 移动到系统路径 /usr/local/bin/ 下以便全局使用
    mv chromedriver /usr/local/bin/ && \
    # 删除已有的 google-chrome 软链接，避免冲突
    rm -f /usr/bin/google-chrome

# https://forum.aspose.com/t/aspose-slides-for-net-no-usable-version-of-libssl-found-with-linux-server/271344/13
# aspose-slides on linux/arm64 is unavailable
# 挂载依赖镜像中的目录到容器内，以便访问所需的 deb 包
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/,target=/deps \
    # 检查当前系统的架构是否为 x86_64 架构
    if [ "$(uname -m)" = "x86_64" ]; then \
    # 对于 x86_64 系统，安装适用于 amd64 架构的 libssl1.1 软件包
        dpkg -i /deps/libssl1.1_1.1.1f-1ubuntu2_amd64.deb; \
    # 如果不是 x86_64，则检查系统架构是否为 aarch64 (ARM 64-bit)
    elif [ "$(uname -m)" = "aarch64" ]; then \
    # 对于 aarch64 系统，安装适用于 arm64 架构的 libssl1.1 软件包
        dpkg -i /deps/libssl1.1_1.1.1f-1ubuntu2_arm64.deb; \
    # 结束条件语句
    fi


# builder stage
FROM base AS builder
# 设置用户
USER root

# 设置工作目录为/ragflow
WORKDIR /ragflow

# install dependencies from uv.lock file
# 复制uv.lock文件到工作目录，准备装换进。
COPY pyproject.toml uv.lock ./

# https://github.com/astral-sh/uv/issues/10462
# uv records index url into uv.lock but doesn't failover among multiple indexes
# 根据 NEED_MIRROR 的值替换 uv.lock 文件中的 PyPI 镜像源地址为阿里云镜像或官方源
RUN --mount=type=cache,id=ragflow_uv,target=/root/.cache/uv,sharing=locked \
    # 如果 NEED_MIRROR 等于 "1"，则将 uv.lock 中的 pypi.org 替换为 mirrors.aliyun.com/pypi
    if [ "$NEED_MIRROR" == "1" ]; then \
        sed -i 's|pypi.org|mirrors.aliyun.com/pypi|g' uv.lock; \
    # 否则（即 NEED_MIRROR 不等于 "1"），将 uv.lock 中的 mirrors.aliyun.com/pypi 替换回 pypi.org
    else \
        sed -i 's|mirrors.aliyun.com/pypi|pypi.org|g' uv.lock; \
    fi; \
    # 如果 LIGHTEN 等于 "1"，则仅安装主依赖项（不带可选扩展包）并使用 Python 3.10 版本进行同步安装
    if [ "$LIGHTEN" == "1" ]; then \
        uv sync --python 3.10 --frozen; \
    # 否则安装主依赖及其所有可选扩展包，严格按照 uv.lock 锁定版本进行依赖同步安装
    else \
        # --frozen：表示严格按照 uv.lock 安装依赖，不更新任何包；--all-extras：安装主依赖及其所有可选扩展包；
        uv sync --python 3.10 --frozen --all-extras; \
    fi

# 复制 web 目录到容器中
COPY web web
# 复制 docs 目录到容器中
COPY docs docs
# 使用缓存挂载方式运行 npm 安装和构建前端项目，提升构建速度
RUN --mount=type=cache,id=ragflow_npm,target=/root/.npm,sharing=locked \
    cd web && npm install && npm run build

# 复制 .git 仓库信息到容器中，用于后续获取版本信息
COPY .git /ragflow/.git

# 获取 Git 描述的版本号，并根据 LIGHTEN 标志添加 "slim" 或 "full" 后缀
RUN version_info=$(git describe --tags --match=v* --first-parent --always); \
    # 如果 LIGHTEN 为 1，则在版本信息后加上 "slim"
    if [ "$LIGHTEN" == "1" ]; then \
        version_info="$version_info slim"; \
    # 否则在版本信息后加上 "full"
    else \
        version_info="$version_info full"; \
    fi; \
    # 打印当前构建的 RAGFlow 版本信息
    echo "RAGFlow version: $version_info"; \
    # 将版本信息写入容器中的 VERSION 文件
    echo $version_info > /ragflow/VERSION

# production stage
# 使用 base 阶段作为基础镜像，定义 production 构建阶段
FROM base AS production
# 切换到 root 用户进行操作
USER root

# 设置工作目录为 /ragflow
WORKDIR /ragflow

# Copy Python environment and packages
# 设置虚拟环境路径
ENV VIRTUAL_ENV=/ragflow/.venv
# 从 builder 阶段复制 Python 虚拟环境到当前镜像中
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
# 将虚拟环境的 bin 目录加入 PATH，方便调用 Python 可执行文件
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"

# 设置 PYTHONPATH 环境变量，使 Python 导入模块时能识别项目目录
ENV PYTHONPATH=/ragflow/

# 复制 web 源码目录到容器中
COPY web web
# 复制 api 源码目录到容器中
COPY api api
# 复制 conf 配置目录到容器中
COPY conf conf
# 复制 deepdoc 模块源码到容器中
COPY deepdoc deepdoc
# 复制 rag 模块源码到容器中
COPY rag rag
# 复制 agent 模块源码到容器中
COPY agent agent
# 复制 graphrag 模块源码到容器中
COPY graphrag graphrag
# 复制 agentic_reasoning 模块源码到容器中
COPY agentic_reasoning agentic_reasoning
# 复制 pyproject.toml 和 uv.lock 文件，用于依赖管理
COPY pyproject.toml uv.lock ./
# 复制 mcp 模块源码到容器中
COPY mcp mcp
# 复制 plugin 模块源码到容器中
COPY plugin plugin

# 复制服务配置模板文件到容器的 conf 目录下
COPY docker/service_conf.yaml.template ./conf/service_conf.yaml.template
# 复制入口脚本 entrypoint.sh 到容器根目录
COPY docker/entrypoint.sh ./
# 给 entrypoint.sh 添加可执行权限
RUN chmod +x ./entrypoint*.sh

# Copy compiled web pages
# 从 builder 阶段复制编译好的前端页面（dist）到容器中
COPY --from=builder /ragflow/web/dist /ragflow/web/dist

# 从 builder 阶段复制版本信息文件 VERSION 到容器中
COPY --from=builder /ragflow/VERSION /ragflow/VERSION
# 安装 debugpy 包以支持远程调试功能,使用pipx不要影响别的换镜
RUN pipx install debugpy
# 设置容器启动时执行的入口脚本
ENTRYPOINT ["./entrypoint.sh"]
